Skip to content

Commit df68cd8

Browse files
committed
Merge branch 'development' into HTTP-PARITY-REGRESSION
2 parents 308401c + 36d00ff commit df68cd8

11 files changed

Lines changed: 289 additions & 48 deletions

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/blazemeter/jmeter-http-plugin/total?style=for-the-badge&link=https%3A%2F%2Fgithub.com%2FBlazemeter%2Fjmeter-http-plugin%2Freleases)
2+
---
13
# BlazeMeter HTTP Plugin for JMeter (HTTP/1.1, HTTP/2, HTTP/3/QUIC)
24

35
---
@@ -309,9 +311,23 @@ Common configurations:
309311
310312

311313
<a id="readme-buffer-capacity"></a>
312-
### Buffer capacity
314+
### Buffer capacity / response store truncation
315+
316+
By default there is no store cap (`blazemeter.http.maxBufferSize` unset → effective **`-1`**): Jetty buffers without a hard fail (unlike Jetty’s own `BufferingResponseListener` 2 MiB default), and the full decoded body is kept in the sample.
317+
318+
When a positive limit applies, behaviour matches Apache JMeter’s
319+
`httpsampler.max_bytes_to_store_per_request`: the sample stays **successful**, only the first N
320+
bytes are stored in response data, `bodySize` reflects the full decoded length, and JMeter logs
321+
`Big response, truncating it to {} bytes` at **DEBUG**. Truncation is skipped while recording.
322+
323+
**Precedence** (first match wins):
324+
325+
1. Plugin `blazemeter.http.maxBufferSize` (aliases `HTTP2Sampler.maxBufferSize` /
326+
`httpJettyClient.maxBufferSize`) if set
327+
2. Else JMeter `httpsampler.max_bytes_to_store_per_request` if set
328+
3. Else `-1` (no truncation)
313329

314-
By default, the size of downloaded resources is limited to 2 MB (2,097,152 bytes); you can raise the limit by setting `blazemeter.http.maxBufferSize` in `jmeter.properties` or `user.properties` (value in bytes).
330+
`<= 0` means do not truncate.
315331

316332

317333
<a id="readme-alpn"></a>
@@ -375,7 +391,7 @@ Restart JMeter after changing JMeter properties that are applied when affected c
375391
| **Attribute** | **Description** | **Default** |
376392
|---|---|---:|
377393
| **blazemeter.http.proxy_enabled** | When **`true`**, the HTTP(S) Test Script Recorder creates **`bzm - HTTP Sampler`** instead of stock **HTTP Request** (legacy `HTTP2Sampler.proxy_enabled` accepted) | true |
378-
| **blazemeter.http.maxBufferSize** | Maximum size of the downloaded resources in bytes | 2097152 |
394+
| **blazemeter.http.maxBufferSize** | Max bytes stored in sample response data (`<=0` / unset default path = no truncation). Overrides JMeter store limit when set | -1 |
379395
| **blazemeter.http.minThreads** | Minimum number of threads per HTTP client | 1 |
380396
| **blazemeter.http.maxThreads** | Maximum number of threads per HTTP client | 5 |
381397
| **blazemeter.http.maxRequestsQueuedPerDestination** | Maximum number of requests that may be queued to a destination | 32767 |

src/main/java/com/blazemeter/jmeter/http2/core/HTTP2FutureResponseListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class HTTP2FutureResponseListener extends BufferingResponseListener
3737
private long responseEnd;
3838

3939
public HTTP2FutureResponseListener() {
40-
this(2 * 1024 * 1024);
40+
this(-1);
4141
}
4242

4343
public HTTP2FutureResponseListener(int maxLength) {

src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.LinkedHashMap;
3939
import java.util.Locale;
4040
import java.util.Map;
41+
import java.util.Properties;
4142
import java.util.Set;
4243
import java.util.concurrent.ConcurrentHashMap;
4344
import java.util.concurrent.ExecutionException;
@@ -67,6 +68,7 @@
6768
import org.apache.jmeter.protocol.http.util.HTTPFileArg;
6869
import org.apache.jmeter.services.FileServer;
6970
import org.apache.jmeter.testelement.property.JMeterProperty;
71+
import org.apache.jmeter.threads.JMeterContextService;
7072
import org.apache.jmeter.util.JMeterUtils;
7173
import org.brotli.dec.BrotliInputStream;
7274
import org.eclipse.jetty.client.AbstractAuthentication;
@@ -147,10 +149,17 @@ public class HTTP2JettyClient {
147149
private static final boolean ADD_CONTENT_TYPE_TO_POST_IF_MISSING = JMeterUtils.getPropDefault(
148150
"http.post_add_content_type_if_missing", false);
149151
// Matches HTTPFileImpl: caps stored response data for file:// samples, while sampleEnd's
150-
// bodySize still reflects the true total bytes read.
152+
// bodySize still reflects the true total bytes read. Separate from HTTP store truncation
153+
// ({@link #maxBufferSize}): JMeter also keeps HTTPFileImpl's 10 MiB default distinct from
154+
// HTTPSamplerBase's default of 0 for the same property name.
151155
private static final int MAX_FILE_SAMPLE_BYTES_TO_STORE = JMeterUtils.getPropDefault(
152-
"httpsampler.max_bytes_to_store_per_request", 10 * 1024 * 1024);
156+
BzmHttpPluginProperties.JMETER_MAX_BYTES_TO_STORE_PER_REQUEST, 10 * 1024 * 1024);
153157
private static final int FILE_SAMPLE_BUFFER_SIZE = 4096;
158+
/**
159+
* Jetty {@code BufferingResponseListener} hard cap. Always unlimited so large bodies are not
160+
* aborted; SampleResult store truncation uses {@link #maxBufferSize} instead (JMeter parity).
161+
*/
162+
private static final int JETTY_BUFFERING_UNLIMITED = -1;
154163
private static final Pattern PORT_PATTERN = Pattern.compile("\\d+");
155164
private static final String MULTI_PART_SEPARATOR = "--";
156165
private static final String LINE_SEPARATOR = "\r\n";
@@ -190,7 +199,12 @@ public class HTTP2JettyClient {
190199
private static final Map<String, Http1OnlyEntry> HTTP1_ONLY_CACHE = new ConcurrentHashMap<>();
191200
private static final Map<String, H2cEntry> H2C_CACHE = new ConcurrentHashMap<>();
192201
private int requestTimeout = 0;
193-
private int maxBufferSize = 21 * 1024 * 1024;
202+
/**
203+
* Max bytes stored in {@link HTTPSampleResult} response data ({@code <= 0} = no truncation).
204+
* Resolved from plugin {@code maxBufferSize} if set, else JMeter
205+
* {@code httpsampler.max_bytes_to_store_per_request} if set, else {@code -1}.
206+
*/
207+
private int maxBufferSize = -1;
194208
private int maxThreads = 5;
195209
private boolean maxThreadsConfigured = false;
196210
private int minThreads = 1;
@@ -717,10 +731,19 @@ public HttpClient getHttpClient() {
717731
return httpClient;
718732
}
719733

734+
/**
735+
* Max bytes kept in the sample response data (JMeter-style store truncation). {@code <= 0} means
736+
* do not truncate.
737+
*/
720738
public int getMaxBufferSize() {
721739
return maxBufferSize;
722740
}
723741

742+
/** Length passed to Jetty buffering listeners; always unlimited so truncation is store-only. */
743+
public static int getJettyBufferingMaxLength() {
744+
return JETTY_BUFFERING_UNLIMITED;
745+
}
746+
724747
public int getRequestTimeout() {
725748
return requestTimeout;
726749
}
@@ -735,9 +758,7 @@ public void loadProperties(HTTP2ClientProfileConfig profileConfig) {
735758
byteBufferPoolFactor =
736759
Integer.parseInt(BzmHttpPluginProperties.getPropDefault(
737760
"httpJettyClient.byteBufferPoolFactor", String.valueOf(byteBufferPoolFactor)));
738-
maxBufferSize =
739-
Integer.parseInt(BzmHttpPluginProperties.getPropDefault("httpJettyClient.maxBufferSize",
740-
String.valueOf(2 * 1024 * 1024)));
761+
maxBufferSize = resolveMaxBytesToStorePerRequest();
741762
minThreads = Integer
742763
.parseInt(BzmHttpPluginProperties.getPropDefault("httpJettyClient.minThreads",
743764
String.valueOf(minThreads)));
@@ -869,6 +890,25 @@ public void loadProperties(HTTP2ClientProfileConfig profileConfig) {
869890
}
870891
}
871892

893+
/**
894+
* Plugin {@code maxBufferSize} wins when set; otherwise JMeter
895+
* {@code httpsampler.max_bytes_to_store_per_request} when set; otherwise {@code -1} (no
896+
* truncation). {@code <= 0} means do not truncate, matching JMeter's store semantics.
897+
*/
898+
private static int resolveMaxBytesToStorePerRequest() {
899+
if (BzmHttpPluginProperties.isDefined(BzmHttpPluginProperties.MAX_BUFFER_SIZE_PROP)) {
900+
return Integer.parseInt(BzmHttpPluginProperties.getPropDefault(
901+
BzmHttpPluginProperties.MAX_BUFFER_SIZE_PROP, "-1").trim());
902+
}
903+
Properties props = JMeterUtils.getJMeterProperties();
904+
if (props != null
905+
&& props.containsKey(BzmHttpPluginProperties.JMETER_MAX_BYTES_TO_STORE_PER_REQUEST)) {
906+
return Integer.parseInt(JMeterUtils.getPropDefault(
907+
BzmHttpPluginProperties.JMETER_MAX_BYTES_TO_STORE_PER_REQUEST, "0").trim());
908+
}
909+
return -1;
910+
}
911+
872912
private boolean getBooleanProp(String key, Boolean overrideValue, boolean defaultValue) {
873913
if (overrideValue != null) {
874914
return overrideValue;
@@ -1575,8 +1615,9 @@ public HTTPSampleResult sample(HTTP2Sampler sampler, HTTPSampleResult result,
15751615
return cacheManager.buildCachedSampleResult(result);
15761616
}
15771617
lowLevelDebug("=== Creating HTTP2FutureResponseListener ===");
1578-
lowLevelDebug("maxBufferSize: {}", maxBufferSize);
1579-
HTTP2FutureResponseListener listener = new HTTP2FutureResponseListener(maxBufferSize);
1618+
lowLevelDebug("maxBytesToStorePerRequest: {}", maxBufferSize);
1619+
HTTP2FutureResponseListener listener =
1620+
new HTTP2FutureResponseListener(JETTY_BUFFERING_UNLIMITED);
15801621
lowLevelDebug("=== HTTP2FutureResponseListener created successfully ===");
15811622
listener.setRequest(request);
15821623
lowLevelDebug("=== About to call send() ===");
@@ -1881,7 +1922,8 @@ private ContentResponse sendWithHappyEyeballs(Request h3Request,
18811922
java.util.concurrent.atomic.AtomicBoolean h2Started =
18821923
new java.util.concurrent.atomic.AtomicBoolean(false);
18831924

1884-
HTTP2FutureResponseListener h2Listener = new HTTP2FutureResponseListener(maxBufferSize);
1925+
HTTP2FutureResponseListener h2Listener =
1926+
new HTTP2FutureResponseListener(JETTY_BUFFERING_UNLIMITED);
18851927
Request h2Request = cloneRequest(h3Request, httpClientNoH3);
18861928
h2Listener.setRequest(h2Request);
18871929

@@ -4007,7 +4049,8 @@ private void setResultContentResponse(HTTPSampleResult result,
40074049
// Decode compressed payloads when possible even if the request did not advertise
40084050
// Accept-Encoding (some servers still compress, and JMeter should show decoded body).
40094051
byte[] responseContent = maybeDecodeCompressedContent(contentResponse);
4010-
// Avoid an extra stream->byte[] copy; content is already fully buffered.
4052+
// JMeter parity: optionally truncate stored response data while keeping full bodySize.
4053+
responseContent = maybeTruncateStoredResponseData(result, responseContent);
40114054
result.setResponseData(responseContent);
40124055

40134056
if (result.getEndTime() == 0) {
@@ -4122,6 +4165,33 @@ private static void debugToFile(String message) {
41224165
}
41234166
}
41244167

4168+
/**
4169+
* Matches {@code HTTPSamplerBase#readResponse}: keep at most {@link #maxBufferSize} bytes in the
4170+
* sample store, log the same debug line JMeter uses, set {@code bodySize} to the full decoded
4171+
* length, and leave the sample successful. Skipped while recording.
4172+
*/
4173+
private byte[] maybeTruncateStoredResponseData(HTTPSampleResult result, byte[] content) {
4174+
if (content == null) {
4175+
return new byte[0];
4176+
}
4177+
int fullLength = content.length;
4178+
if (maxBufferSize <= 0 || fullLength <= maxBufferSize || isJMeterRecording()) {
4179+
return content;
4180+
}
4181+
// Same message as Apache JMeter HTTPSamplerBase.readResponse (debug level).
4182+
LOG.debug("Big response, truncating it to {} bytes", maxBufferSize);
4183+
result.setBodySize(fullLength);
4184+
return Arrays.copyOf(content, maxBufferSize);
4185+
}
4186+
4187+
private static boolean isJMeterRecording() {
4188+
try {
4189+
return JMeterContextService.getContext().isRecording();
4190+
} catch (RuntimeException e) {
4191+
return false;
4192+
}
4193+
}
4194+
41254195
private void resetSamplerDataBeforeResultProcessing(HTTPSampleResult result) {
41264196
if (result == null) {
41274197
return;

src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRe
402402
this.result.setIgnore();
403403
}
404404
HTTP2FutureResponseListener listener =
405-
new HTTP2FutureResponseListener(client.getMaxBufferSize());
405+
new HTTP2FutureResponseListener(HTTP2JettyClient.getJettyBufferingMaxLength());
406406
this.asyncListener = listener;
407407
Request req = client.sampleAsync(this, this.result, listener);
408408
req.send(listener); // Fire the Async

src/main/java/com/blazemeter/jmeter/http2/util/BzmHttpPluginProperties.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ public final class BzmHttpPluginProperties {
2929
/** Legacy prefix for async-controller-related JMeter properties (backward compatibility). */
3030
public static final String CONTROLLER_LEGACY_PREFIX = "http2AsyncController.";
3131

32+
/**
33+
* Plugin property for max bytes stored in HTTP sample response data (any accepted prefix form).
34+
* Aliases: {@code blazemeter.http.maxBufferSize}, {@code HTTP2Sampler.maxBufferSize},
35+
* {@code httpJettyClient.maxBufferSize}.
36+
*/
37+
public static final String MAX_BUFFER_SIZE_PROP = "httpJettyClient.maxBufferSize";
38+
39+
/**
40+
* Stock JMeter property for max response bytes stored per request (HTTPSamplerBase /
41+
* HTTPFileImpl).
42+
*/
43+
public static final String JMETER_MAX_BYTES_TO_STORE_PER_REQUEST =
44+
"httpsampler.max_bytes_to_store_per_request";
45+
3246
private static final String PREFERRED_PREFIX = "blazemeter.http.";
3347
private static final String LEGACY_SAMPLER_PREFIX = "HTTP2Sampler.";
3448
private static final String LEGACY_JETTY_PREFIX = "httpJettyClient.";
@@ -70,6 +84,14 @@ private static String[] allKeysInResolveOrder(String propertyName) {
7084
};
7185
}
7286

87+
/**
88+
* All accepted JMeter property keys for a plugin setting (preferred + legacy aliases), for
89+
* tests or cleanup that must remove every form.
90+
*/
91+
public static String[] keysInResolveOrder(String propertyName) {
92+
return allKeysInResolveOrder(propertyName);
93+
}
94+
7395
/** Suffix after {@link #CONTROLLER_PREFERRED_PREFIX} or {@link #CONTROLLER_LEGACY_PREFIX}. */
7496
public static String controllerCanonicalSuffix(String propertyName) {
7597
if (propertyName.startsWith(CONTROLLER_PREFERRED_PREFIX)) {

src/test/java/com/blazemeter/jmeter/http2/core/HTTP2FutureResponseListenerTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public void setUp() {
3030
listener.setRequest(mockRequest);
3131
}
3232

33+
@Test
34+
public void defaultConstructorUsesUnlimitedBufferingCapacity() {
35+
// Jetty BufferingResponseListener's no-arg default is 2 MiB; plugin default must stay -1
36+
// (DynamicCapacity maps that to Long.MAX_VALUE) so large responses are not rejected.
37+
assertEquals(Long.MAX_VALUE, listener.getMaxLength());
38+
}
39+
3340
@Test
3441
public void getRequestReturnsSetRequest() {
3542
assertEquals(mockRequest, listener.getRequest());

0 commit comments

Comments
 (0)